/**
 *
 * 18所测试报告
 *
 * Created by nongzhizhong on 2025/04/23
 *
 */
const path = require('path');
const fs = require('fs-extra');
const moment = require('moment');
const readline = require('readline');
const compressing = require('compressing');
const { spawn } = require('child_process');
const ProgressIndicator = require('../../../progress-indicator');
const BaseReporter = require('../base-report');
const pkgJson = require('../../../../../package.json');
const classCreator = require('../base-report-test-case-transformer');
const utility = require('../../../utility');
const tests = require('../../../tests');
const functionRelations = require('../../../function-relations');
const invokeRelations = require('../../../invoke-relations');
const logger = require('../../../logger');
const files = require('../../../files');
const functions = require('../../../functions');

class DefaultReporter extends BaseReporter {
  constructor(versionId, locale, version = null, project = null, creator = null, outputDir = null, format, complete = false) {
    super(versionId, locale, version, project, creator, outputDir, __dirname, format);
    this.reportData = {
      files: [],
      numberOfLines: 0,
      testcaseCount: 0,
      passTestcaseCount: 0,
      noPassTestcaseCount: 0,
      maxInvokeFuncDepth: 0,
      toolVersion: pkgJson.version,
      userName: this.creator.username,
      language: this.version.language,
      fanOutGt7OrcycleComplexityGt10: [],
      projectName: this.project.projectName,
      versionName: this.version.versionName,
      reportTime: moment().format('YYYY/MM/DD'),
      invokedFlowImage: '',
      invokedFlowData: {
        nodes: new Map(),
        edges: new Set(),
      },
    };
    this.fileSuffixes = [];
    this.ReportTestCaseTransformer = classCreator(this.version.versionType);
    this.fileIndex = 1;
    this.complete = complete;
    this.funcRelationsOfVersions = [];
  }

  async generate(progressIndicator = new ProgressIndicator()) {
    const result = {
      cachePath: this.outputDir,
    };
    this.handleFileSuffixes();
    this.versionFuncsMap = await functions.getFunctionByVersionId(this.versionId, ['_id', 'functionName', 'fanIn']);
    this.funcRelationsOfVersions = await functionRelations.getRelationsByVersion(this.versionId);
    const [filesProgressIndicator, generateImageProgressIndictor, generateReportProgressIndictor] = progressIndicator.split([0.8, 0.1, 0.1]);
    const { versionDir } = await this.spliteByFile(null, filesProgressIndicator);
    await this.generateFuncsFlowChart();
    generateImageProgressIndictor.finish();
    await this.generateSummaryReport('test-analyze-template', this.reportData, path.join(versionDir, '单元测试分析报告（公开）'));
    await this.generateSummaryReport('test-description-template', this.reportData, path.join(versionDir, '单元测试说明报告（公开）'));
    const fileName = `${this.project.projectName}-${this.version.versionName}-${this.getReportName()}-${this.format}.zip`;
    result.reportPath = path.join(this.outputDir, fileName);
    const reportsDir = path.join(this.outputDir, this.project.projectName);
    await compressing.zip.compressDir(reportsDir, result.reportPath);
    generateReportProgressIndictor.finish();
    return result;
  }

  handleFileSuffixes() {
    if (!this.version.languages) {
      return;
    }
    const { languages } = this.version;
    Object.keys(languages).forEach((languageName) => {
      if (!languages[languageName].suffixes) {
        return;
      }
      this.fileSuffixes.push(...languages[languageName].suffixes);
    });
  }

  generateSummaryReport(template, data, output) {
    return this.generateReport(template, data, output);
  }

  /**
         * 计算百分比
         * @param {Number} numerator 分子
         * @param {Number} denominator 分母
         * @returns
         */
  handlePercent(numerator, denominator) {
    if (denominator === 0) {
      throw new Error('Denominator cannot equal to 0');
    }
    if (numerator === 0) {
      return '0%';
    }
    let precent = (numerator / denominator) * 100;
    if (precent.toString().indexOf('.') !== -1 && precent.toString().split('.')[1].length > 2) {
      precent = precent.toFixed(2);
    }
    return `${precent}%`;
  }

  async handleFile(file, versionDir) {
    if (!this.fileSuffixes.includes(file.extname)) {
      return;
    }
    const collectFileData = {
      fileIndex: this.fileIndex,
      functions: [],
      filePath: file.path,
      fileName: file.fileName,
      fanInAndFanOutEqualZero: [],
      emptyLines: file.emptyLines,
      sourceLines: file.sourceLines,
      commentLines: file.commentLines,
      numberOfLines: file.numberOfLines,
      commentPercent: this.handlePercent(file.commentLines, file.numberOfLines),
    };
    this.reportData.numberOfLines += file.numberOfLines;
    const { functions: funcs, typeSystem } = await super.handleFile(file, versionDir);
    if (!funcs || funcs.length === 0) {
      return;
    }
    const versionCoverages = this.version.coverages;
    const fileComments = await this.getFileComment(file);
    await utility.arrayChunkOperation(funcs, 30, async (func) => {
      func.fileName = file.fileName;
      const funcLine = func.index;
      const collectFuncData = {
        params: '无',
        testcases: [],
        returnType: '无',
        line: funcLine,
        testcaseIndex: '',
        mcdcCoverage: '-',
        branchCoverage: '-',
        commentFuncName: '无',
        passTestcaseCount: 0,
        commentFuncAction: '无',
        statementCoverage: '-',
        noPassTestcaseCount: 0,
        functionName: func.functionName,
      };
      if (fileComments[funcLine]) {
        fileComments[funcLine].forEach((commentLine) => {
          const commentFuncNameStr = commentLine.match(/^函数名称：(.*)/);
          const commentFuncActionStr = commentLine.match(/^函数功能：(.*)/);
          if (commentFuncNameStr && commentFuncNameStr.length >= 2) {
            [, collectFuncData.commentFuncName] = commentFuncNameStr;
            return;
          }
          if (commentFuncActionStr && commentFuncActionStr.length >= 2) {
            [, collectFuncData.commentFuncAction] = commentFuncActionStr;
          }
        });
      }
      const funcVariables = func.variables;
      if (funcVariables) {
        if (funcVariables.variables
          && funcVariables.variables.params
          && Object.keys(funcVariables.variables.params).length > 0) {
          collectFuncData.params = Object.keys(funcVariables.variables.params).toString();
        }
        if (funcVariables.output && funcVariables.output['%'] && funcVariables.output['%']['@type']) {
          collectFuncData.returnType = `return ${funcVariables.output['%']['@type']}`;
        }
      }
      if ((versionCoverages & tests.coverageKind.statement) && func.statementTotal) {
        collectFuncData.statementCoverage = this.handlePercent(func.statementCovered, func.statementTotal);
      }
      if ((versionCoverages & tests.coverageKind.branch) && func.branchTotal) {
        collectFuncData.branchCoverage = this.handlePercent(func.branchCovered, func.branchTotal);
      }
      if ((versionCoverages & tests.coverageKind.mcdc) && func.mcdcTotal) {
        collectFuncData.mcdcCoverage = this.handlePercent(func.mcdcCovered, func.mcdcTotal);
      }
      if (func.fanIn === func.fanOut && func.fanIn === 0) {
        collectFileData.fanInAndFanOutEqualZero.push({ funcName: func.functionName });
      }
      if (func.fanOut > 7 || func.cycleComplexity > 10) {
        this.reportData.fanOutGt7OrcycleComplexityGt10.push(
          {
            funcName: func.functionName,
            fanOut: func.fanOut,
            cycleComplexity: func.cycleComplexity,
          }
        );
      }
      if (!this.reportData.invokedFlowData.nodes.has(func.functionName)) {
        const node = {
          funcName: func.functionName,
          type: 'defined',
          fanIn: func.fanIn,
        };
        this.reportData.invokedFlowData.nodes.set(func.functionName, node);
      }
      let relations = await functionRelations.retrieveRelations(
        func._id,
        this.versionId,
        this.funcRelationsOfVersions
      );
      relations = await invokeRelations.filterRelations(func._id, relations, { includeCtor: false, completeDepth: true });
      let maxDepth = 0;
      relations.forEach((relationFunc) => {
        const {
          toFuncId, toFuncName, fromFuncId, depth, toFuncPointer,
        } = relationFunc;
        maxDepth = Math.max(depth.maxDepth, maxDepth);
        let node = {
          funcName: toFuncName,
          level: depth.maxDepth,
          type: toFuncId ? 'defined' : 'undefined',
        };
        if (toFuncPointer) {
          node.type = 'funcpointer';
        }
        if (this.reportData.invokedFlowData.nodes.has(toFuncName)) {
          node = this.reportData.invokedFlowData.nodes.get(toFuncName);
          node.level = node.level ? Math.max(node.level, depth.maxDepth) : depth.maxDepth;
        } else {
          this.reportData.invokedFlowData.nodes.set(toFuncName, node);
        }
        this.reportData.invokedFlowData.edges.add(`${this.versionFuncsMap.get(fromFuncId.toString()).functionName}|${toFuncName}`);
      });
      this.reportData.maxInvokeFuncDepth = Math.max(this.reportData.maxInvokeFuncDepth, maxDepth);
      let testcaseIndex = 1;
      func.tests.forEach((testcase) => {
        if (testcase.resultKind === tests.resultKind.unknown) {
          return;
        }
        const collectTestcaseData = {
          input: [],
          output: [],
          resultKind: '',
          index: `${'0'.repeat(3 - testcaseIndex.toString().length)}${testcaseIndex}`,
        };
        this.reportData.testcaseCount += 1;
        if ([tests.resultKind.succeed, tests.resultKind.noResult, tests.resultKind.nonStrictSucceed].includes(testcase.resultKind)) {
          this.reportData.passTestcaseCount += 1;
          collectFuncData.passTestcaseCount += 1;
          collectTestcaseData.resultKind = 'pass';
        } else {
          this.reportData.noPassTestcaseCount += 1;
          collectFuncData.noPassTestcaseCount += 1;
        }
        if (testcase.resultKind === tests.resultKind.failed) {
          collectTestcaseData.resultKind = 'noPass';
        }
        if ([tests.resultKind.runtimeError, tests.resultKind.timeout].includes(testcase.resultKind)) {
          collectTestcaseData.resultKind = 'RuntimeError';
        }

        if ([tests.resultKind.systemError, tests.resultKind.instrumentError, tests.resultKind.compileError].includes(testcase.resultKind)) {
          collectTestcaseData.resultKind = 'executeException';
        }
        const testcaseDetails = this.getTestcaseDetails(testcase, funcVariables, typeSystem, func);
        collectTestcaseData.input = testcaseDetails.input;
        collectTestcaseData.output = testcaseDetails.output;
        collectFuncData.testcases.push(collectTestcaseData);
        testcaseIndex += 1;
      });
      if (collectFuncData.testcases.length > 0) {
        const len = collectFuncData.testcases.length;
        collectFuncData.testcaseIndex = len > 1 ? `001-${'0'.repeat(3 - len.toString().length)}${len}` : '001';
      }
      if (collectFuncData.testcases.length === 0) {
        return;
      }
      collectFileData.functions.push(collectFuncData);
    });
    if (collectFileData.functions.length === 0) {
      return;
    }
    this.fileIndex += 1;
    collectFileData.functions.sort((a, b) => a.line - b.line);
    collectFileData.functions.forEach((func, index) => {
      func.funcIndex = index + 1;
    });
    this.reportData.files.push(collectFileData);
  }

  getTestcaseDetails(testcase, variables, typeSystem, func) {
    const resDetails = {
      input: [],
      output: [],
    };
    const transformer = new this.ReportTestCaseTransformer(
      tests.getJson(testcase, this.i18n),
      variables,
      typeSystem,
      this.i18n
    );
    try {
      transformer.transform(this.complete);
    } catch (error) {
      logger.error(`testCase transform error: fileName: ${func.fileName} function name: ${func.functionName} test case number: ${testcase.number}:`, error);
      return resDetails;
    }
    // eslint-disable-next-line no-control-regex
    const unusualCharReg = /[\x00-\x08\x0B\x0C\x0E-\x1F]/g;
    if (utility.isArrayType(transformer.input)) {
      transformer.input.forEach((input) => {
        input.variable = input.variable.replace(unusualCharReg, '');
        // 桩函数代码打桩
        if (input.values) {
          delete input.value;
          resDetails.input.push({
            ...input,
            variable: `代码打桩${input.variable}`,
          });
          return;
        }
        // 处理变量值显示格式
        if (input.value !== '') {
          input.value = ` = ${input.value}`;
        }
        if (input.category && input.variable) {
          resDetails.input.push({
            category: `${input.category}:`,
            value: '',
            variable: '',
          });
          resDetails.input.push({
            category: '',
            value: input.value,
            variable: input.variable,
          });
        } else {
          resDetails.input.push(input);
        }
      });
    }
    if (utility.isArrayType(transformer.output)) {
      transformer.output.forEach((output) => {
        output.variable = output.variable.replace(unusualCharReg, '');
        // eslint-disable-next-line no-nested-ternary
        output.value = output.value !== '' ? ` = ${output.value}` : (output.result !== '' ? ` = ${output.result}` : '');
        if (output.result !== '') {
          output.result = ` = ${output.result}`;
        }
        if (output.category && output.variable) {
          resDetails.output.push({
            category: `${output.category}:`,
            value: '',
            result: '',
            variable: '',
          });
          resDetails.output.push({
            category: '',
            value: output.value,
            result: output.result,
            variable: output.variable,
          });
        } else {
          resDetails.output.push(output);
        }
      });
    }
    return resDetails;
  }

  async getFileComment(file) {
    const fileStream = await files.getFileContentStream(null, file, true);
    if (!fileStream) {
      return [];
    }
    const comments = {};
    const rl = readline.createInterface({
      input: fileStream,
      crlfDelay: Infinity,
    });
    let lineNumber = 0;
    let inBlock = false;
    let currentComment = [];
    // eslint-disable-next-line no-restricted-syntax
    for await (const line of rl) {
      lineNumber += 1;
      if (!inBlock) {
        const startIdx = line.indexOf('/*');
        if (startIdx !== -1) {
          const endIdx = line.indexOf('*/', startIdx + 2);
          // 忽略单行注释只取多行注释
          if (endIdx === -1) {
            // 多行注释开始
            inBlock = true;
            currentComment.push(line.slice(startIdx).replace(/^\/\*+/, '').trim());
          }
        }
      } else {
        const endIdx = line.indexOf('*/');
        if (endIdx !== -1) {
          currentComment.push(line.slice(0, endIdx + 2).replace(/\*+\/$/, '').trim());
          comments[lineNumber + 1] = currentComment.filter((i) => i);
          inBlock = false;
          currentComment = [];
        } else {
          currentComment.push(line.replace(/^( )?\*+/g, '').trim());
        }
      }
    }
    return comments;
  }

  getImageSize() {
    return {
      invokedFlowImage: {
        width: 800,
        height: 600,
      },
    };
  }

  /**
  *  mermaid-cli 生成图片
  * @param {String} inputPath  输入xxx.md格式的图表文件，具体格式npx mmdc -h
  * @param {String} outputPath 输出路径
  * @param {String} fileName 输出文件名
  * @param {String} extname  输出的文件扩展名
  * @param {String} key  标记图片的key
  */
  async generateFlowImage(inputFilePath, outputPath, fileName, extname, key) {
    const mmdcpath = `"${path.join(__dirname, '../', '../', '../', '../', '../', 'node_modules', '.bin', 'mmdc')}"`;
    const puppeteerConfig = `"${path.join(__dirname, 'puppeteer-config.json')}"`;
    const mermaidConfig = `"${path.join(__dirname, 'mermaid-config.json')}"`;
    await new Promise((resolve, reject) => {
      const mmdc = spawn(mmdcpath, [
        '-i', inputFilePath,
        '-o', path.join(outputPath, `${fileName}.${extname}`),
        '-p', puppeteerConfig,
        '-c', mermaidConfig,
        '-w', 4000,
        '-H', 3000,
        '-s', 2,
      ], { shell: true });
      mmdc.stderr.on('data', (err) => {
        logger.info(`mmdc stderr: ${err}`);
        reject(err);
      });
      mmdc.on('error', (error) => {
        logger.info(`mmdc error: ${error}`);
        reject(error);
      });
      mmdc.on('close', (code) => {
        logger.info(`mmdc generate ${key} ${extname}:${code === 0 ? 'success' : 'failed'}`);
        resolve();
      });
    });
    // rename output filename
    await new Promise((resolve, reject) => {
      const oldFileName = path.join(outputPath, `${fileName}-1.${extname}`);
      const newFileName = path.join(outputPath, `${fileName}.${extname}`);
      fs.rename(oldFileName, newFileName, (err) => {
        if (err) {
          reject(new Error(`rename file name:${oldFileName} error:${err}`));
        }
        resolve();
      });
    });
    // delete input xxxx.md
    await new Promise((resolve, reject) => {
      fs.unlink(inputFilePath, (err) => {
        if (err) {
          reject(new Error(`delete input ${inputFilePath} error:${err}`));
        }
        resolve();
      });
    });
  }

  async generateFuncsFlowChart() {
    const nodeNameToNumberMap = new Map();
    const topNode = {
      funcName: '0',
      number: 1,
      type: '',
      level: 0,
    };
    const flowChartData = {
      nodes: [topNode],
      edges: [],
    };
    nodeNameToNumberMap.set(topNode.funcName, topNode.number);
    const { nodes, edges } = this.reportData.invokedFlowData;
    if (!nodes.size || !edges.size) {
      return;
    }
    nodes.forEach((value,) => {
      if (value.fanIn === 0) {
        value.level = 1;
        edges.add(`0|${value.funcName}`);
      }
      flowChartData.nodes.push(value);
    });
    // 函数节点排序
    flowChartData.nodes.sort((a, b) => a.level - b.level);
    this.reportData.invokedFlowData.nodes = [];
    delete this.reportData.invokedFlowData.edges;
    // 对节点进行层级分组
    const newNodes = [];
    flowChartData.nodes.forEach((node, index) => {
      if (!node.number) {
        node.number = index + 1;
        nodeNameToNumberMap.set(node.funcName, node.number);
      }
      if (node.level !== 0) {
        let type = '';
        switch (node.type) {
          case 'defined': {
            type = '已定义';
            break;
          }
          case 'undefined': {
            type = '未定义';
            break;
          }
          case 'funcpointer': {
            type = '函数指针';
            break;
          }
          default: break;
        }
        this.reportData.invokedFlowData.nodes.push({ ...node, type });
      }
      if (!newNodes[node.level]) {
        newNodes[node.level] = [];
      }
      newNodes[node.level].push(node);
    });
    flowChartData.nodes = newNodes;
    edges.forEach((value) => {
      const [fromFuncName, toFuncName] = value.split('|');
      const fromNodeNumber = nodeNameToNumberMap.get(fromFuncName);
      const toNodeNumber = nodeNameToNumberMap.get(toFuncName);
      let line = '---';
      if (fromFuncName === '0') {
        flowChartData.edges.push({ from: fromNodeNumber, to: toNodeNumber, line });
        return;
      }
      if (!nodes.has(fromFuncName) || !nodes.has(toFuncName)) {
        return;
      }
      // 下层函数调用上层函数使用虚线
      if (nodes.get(fromFuncName).level > nodes.get(toFuncName)) {
        line = '-.-';
      }
      flowChartData.edges.push({ from: fromNodeNumber, to: toNodeNumber, line });
    });
    const versionId = this.versionId.toString();
    const outputPath = path.join(this.outputDir, versionId);
    const mdFilePath = `${outputPath}.md`;
    const imageFilePath = `${outputPath}.png`;
    const markdownFileData = await utility.getDotTemplate(path.join(__dirname, 'template', 'dot', 'function-flow-chart-template.md.dot'), flowChartData);
    await utility.writeFile(markdownFileData, mdFilePath);
    await this.generateFlowImage(mdFilePath, this.outputDir, versionId, 'png', `${this.project.projectName}:${this.version.versionName}`);
    this.reportData.invokedFlowImage = imageFilePath;
  }
}
module.exports = DefaultReporter;
